InnoDB源码分析

文档编写历史

开始编写: 2019-01-08 Robert
初稿完成: 2019-03-27 Robert

基于MySQL5.7.24代码分支。

代码分析的5个根本问题

从大的方向,列出了MySQl内核分析的5个问题。依据这5个问题,可以先对mysql各模块、数据结构、函数做一个归类,然后至顶向下求精,逐步吃透。

Alt text

1. InnoDB代码的特点

1.1 代码组织

1.每个目录对应一个大模块,目录下面包含若干个子模块。比如:
log 目录对应处理redolog的代码, 其中有两个模块:
1.log0log.cc: 全局redolog buffer维护, 将redolog刷盘;
2.log0recv.cc: mysql启动时加载redolog来恢复page。

2.cc 文件的命名规则是 大模块名 + 从0开始的阿拉伯数字 + 子模块名; 如果一个子模块有多个.cc文件, 则中间的阿拉伯数字将从0递增。 目前5.7的代码,还没有一个子模块用到了一个以上的.cc文件。
一般来说, 一个.cc文件对应有一个.h文件, 在.h文件中会列出该.cc文件(子模块)被其他子模块调用的函数。 如果一个函数只在.cc文件内被调用,则不会出现在.h文件中。

1.2 阅读难度

InnoDB存储引擎的阅读难度,在我读过的存储引擎中是最大的。 其难度在于:
1.截止到MySQL5.7,InnoDB的主要数据结构和主干函数,并未稳定下来。比如redolog相关结构和代码,5.6和5.7有很大不一样。因此InnoDB本身就还是一个不断演进中的系统;

2.InnoDB的模块间交互,没有一个明显的层次结构, 而是网状的相互调用。这就很难建立一个统一的层次模型,然后从上往下,从粗到细地去梳理代码;

3.在近二十年的演进过程中,出于时空效率的考虑(包括增加一些新的功能,如gis,fts等),InnoDB在最早主干代码的基础上,增加了很多功能点,包括:ibuf,压缩,group commit,不同粒度的page,fts,gis等。在增加这些功能点时,innodb好像并不去考虑到代码的可维护性和可读性。
好的做法是在每增加一个新的功能点时,能够在原来的主干流程中找到一个很小的切口, 通过这个切口把主干运行流程,引导到新功能专门对应的代码包里面,在这个代码包里面完成所有新功能的处理逻辑。但观察InnoDB的代码,我们发现大量新功能是直接在主干运行流程上通过大量if-else添加的,显得分散和凌乱。大多数InnoDB的主干函数,都呈现出这样的观感:

Alt text

4.InnoDB本身逻辑的复杂度。这里面节选了阿里数据库-数据库内核月报上关于fsp模块分配一个page到某个segment的逻辑,可以看出InnoDB演进近20年来,在一些关键流程上的做的非常细致的优化:

Alt text

套用目前流行的说法,innodb的代码是非常硬核的。所谓的硬核就是说不是那么直观易懂的,也不可能把innodb代码完全load到大脑里。 因此,要搞懂InnoDB代码,应该拿出工程的办法,以团队合作的方式去解决。

2. InnoDB内各线程概览

了解一个分布式后台系统,有经验的后台开发会先从运维层面去看系统的部署、流量的走向、模块间的交互,而不是先看系统源码。同样,当我们要梳理InnoDB源码时,首先应该从InnoDB正常运行时启动的线程、各类线程的作用、每类线程的工作流程搞清楚。有了这个信息,再去分析各类线程涉及的主干代码,搞清楚其中的数据结构和函数调用。

下面详细介绍, 一个正常运行的Innodb引擎的五类主要线程。

2.1 用户会话线程:session thread

不考虑线程池的情况下, MySQL为每一个客户端连接开启一个线程,该连接后续发往MySQL的所有SQL请求,均由该线程处理。我们称这种线程为用户会话线程(session thread)。

session thread的特点是一捅到底。对于一条写SQL(insert/update/delete),session thread执行涉及该写SQL处理的所有代码。包括:
1.读取客户端->MySQL的网络包,从中解析出SQL语句;
2.parse sql,然后调用ha_write_row,进入到InnoDB引擎的代码;
3.在innodb中找到row对应的B树的page修改并生成redolog(假设要写的page在内存中,如不在,则去磁盘读取出);
4.如果该SQL是自动提交,则将redolog写入到磁盘;
5.依次返回,最后将操作结果返回给客户端。

Alt text

而对于一条读SQL,也是由session thread执行处理该SQL的所有代码。包括:
1.读取客户端->MySQL的网络包,从中解析出SQL语句;
2.parse sql,优化并生成执行计划;
3.在计划执行时不断调用ha_rnd_next,通过此函数进入到InnoDB引擎的代码;
4.在innodb中找到B树的page,并读取row数据;
4.如果page不在buffer中,则直接从磁盘读取;
5.将读取到的结果最后返回到客户端。

Alt text

到此我们会有一个疑问:如果一个MySQL进程同时处理5000并发select,如果这些select都需要去磁盘读取page,这就意味着5000个线程同时进行磁盘io,这显然是不可接受的。 而InnoDB解决这个问题采用了一个非常简单粗暴的方法: 限流。

通过innodb_thread_concurrency参数,来控制同时进入InnoDB引擎的session thread个数。每个session thread在进入InnoDB之前都需要先递增全局计数,并为当前session thread分配innodb_concurrency_tickets个ticket。如果当前session thread需要进出InnoDB很多次(例如一个大查询需要扫描很多行数据时),在innodb_concurrency_tickets次内,都可以自由进入InnoDB,无需判断innodb_thread_concurrency。当ticket用完时,就需要重新进入;当SQL执行完成后,会将ticket重置为0。

如果当前InnoDB的并发度已满,用户线程就需要等待,目前的实现使用sleep一段时间的方式,sleep的时间是自适应的,可以通过参数innodb_adaptive_max_sleep_delay来设置一个最大sleep事件,具体的算法参阅函数srv_conc_enter_innodb_with_atomics。

2.2 后台线程srv master thread

所谓的后台线程,是和session thread相对应的。session thread负责处理客户端发起的操作,我们可以将其视为前台线程;而在处理执行客户SQL之外,还需要做其他系统层面的事情,比如刷脏页,做check point,undolog purge等(而细究这些事情的实质,其实出发点都是一个:优化数据库执行的时空效率)。

后台线程srv master thread,顾名思义,是最主要的后台线程。在InnoDB的早期版本中,srv master thread确实如此,它几乎承包了InnoDB内需要做的所有后台操作,包括redolog刷盘,脏page刷盘和做check point,合并ibuf,undo log purge等。但是InnoDB在发展过程中,逐渐将其中部分操作剥离出来,用专门的后台线程来执行。在mysql5.7版本中,srv master thread负责的工作有:
1.定时刷redolog
2.合并insert buffer(change buffer)
3.做checkout point

2.3 后台线程page cleaner thread

后台线程page cleaner thread系从srv master thread中剥离出来,专做一件事情:刷脏页。page cleaner thread实际上包含两种线程,入口函数分别是buf_flush_page_cleaner_coordinator和buf_flush_page_cleaner_worker,后者是刷脏页的实际工作线程,后者是管理工作线程的调度线程。

2.4 后台线程purge thread

后台线程purge thread用来做undo log的清理。实际也包含两种线程,入口函数分别是srv_purge_coordinator_thread和srv_worker_thread。 srv_worker_thread负责实际操作,srv_purge_coordinator_thread负责调度。而特别需要指出的是,srv_worker_thread并不是undo log purge的专属线程,它实际上是一个通用的工作线程,可以执行包括undo log purge在内的后台任务。但目前mysql5.7中,只有undo log purge用到了该通用线程。

2.5 MySQL主线程Main Thread

MySQL启动时,入口函数为main的主线程负责对MySQL实例进行初始化,比如加载配置文件,创建监听端口,初始化innoDB实例等。在对InnoDB做初始化时,如果MySQL实例上次并非正常退出,则可能需要根据redolog重新恢复page。该操作入口函数为:recv_recovery_from_checkpoint_start

3. 用户会话线程:SessionThread剖析

在接下来,我们分别考察每一类线程的相关代码,包括代码模块和模块的作用、模块间相互依赖关系、关键数据结构、核心函数等几个方面。首先来看最核心,也是最复杂的线程:SessionThread。

3.1 代码模块

首先给出SessionThread涉及的诸代码模块交互示意图:

Alt text

ha_innobase: ha_innobase是innobase的接口对象, 继承自MySQL存储引擎接口handler,实现了handler的所有接口,供SQL层调用; 在MySQL中,会为每一个table对象生成一个handler对象,这个handler对象只供该table对象使用。
handlerton: handleton是innobase全局接口对象,全局只有1个handleton,该对象提供诸如事务提交、事务回滚等操作。

row0*: row0* 可以理解为InnoDB的业务逻辑层,ha_innobase的各接口,最终将调用row0*的函数,来实现接口的操作逻辑。以ha_innobase::write_row为例:
1.在write_row中首先判断是否能够执行该write_row,如果当前MySQL实例被设置为只读,则不可执行write_row;
2.write_row调用row_insert_for_mysql, 执行该write row操作;
3.row_insert_for_mysql完成后,执行一些后处理逻辑,比如,如果此次write_row包含自增id,则更新dict对象中的自增id最大值等。

接下来考察row_insert_for_mysql这个函数的调用逻辑:
1.首先我们看到,row_insert_for_mysql的参数有一个prebuilt,该对象可以理解为一个执行环境对象,在后续row insert过程中,会把一些临时信息填入到prebuilt,同时根据prebuilt的设置来走不同的逻辑。
2.row_insert_for_mysql往下2层的函数调用是这样的:
row_insert_for_mysql //位于row0mysql.cc
row_insert_for_mysql_using_ins_graph //位于row0mysql.cc
row_ins_step //位于row0ins.cc

三层函数的定义分别是:

dberr_t
row_insert_for_mysql(
const byte* mysql_rec,
row_prebuilt_t* prebuilt)
static
dberr_t
row_insert_for_mysql_using_ins_graph(
const byte* mysql_rec,
row_prebuilt_t* prebuilt)
que_thr_t*
row_ins_step(
/*=========*/
que_thr_t* thr) /*!< in: query thread */

这三层函数的调用,其实经历了从row0mysql到row0ins模块的切换过程。 在历史上,InnoDB的作者,是打算将InnoDB作为一款独立的RDBMS来发布的,因此在InnoDB中会有一个一个pars模块,里面包含一个独立的词法语法解析器,而row_ins_step的que_thr_t* thr参数,可以理解为InnoDB内部的客户端session结构(对应MySQL SQL层的THD结构)。
因此,row_ins_step 函数,才是InnoDB提供的,真正的记录插入函数;而row_insert_for_mysql 和row_insert_for_mysql_using_ins_graph,是InnoDB为了适配MySQL开发的,介于真正InnoDB核心和MySQL之间的适配层。

从这三个函数和相关参数的命名来看,可以看出上面的考虑。 row_insert_for_mysql 和row_insert_for_mysql_using_ins_graph 中包含for_mysql , 表明它们适配mysql SQL层的身份; 而row_prebuilt_t是工作于row0mysql这个MySQL适配模块的数据结构,并不会到row0ins这样的InnoDB核心层里去; 对于InnoDB核心层,每个其入口函数的一般都以 _step 为后缀,比如row_ins_step, row_upd_step 等。

考察row_ins_step, row_upd_step 这样的入口函数和调用机制,才能真正看出InnoDB作者当年想把InnoDB建设为一个独立RDBMS的蓝图。 所以说, row_ins_step等函数绝不是一个简单的insert操作,被row0mysql层调用,传入要insert的row,返回操作结果这么简单。 row_ins_step等函数,和que0que以及pars这两个模块结合在一起分析,才能看出InnoDB最核心的代码构架思路。

可能很多同学看过彭立勋早期写过的这一个ppt,里面对InnoDB内各模块结构做了介绍:

Alt text
这页ppt对pars和que的提法,是有大问题的。 que并非可有可无,而是真正理解InnoDB代码构架的钥匙。
que模块和query graph这个概念,实际上是InnoDB在把自己作为一个独立RDBMS时,将SQL语句做词法语法解析后翻译而成的一个内部结构。
更加能够清晰说明这一点的,是que_eval_sql这个函数:

Alt text

这个函数的具体代码如上。粗看之下,这段代码描述了InnoDB解析一个sql,生成执行计划并执行的过程。que_eval_sql的sql参数的具体内容,往往是这样的:

Alt text

在InnoDB的SQL语法规范设计中,每一种SQL语句都是包装在一个过程(函数)内。que_eval_sql内部调用parse_sql解析该过程,生成一个query graph,然后交由que_run_threads这个执行引擎去处理。 不要被_threads整个后缀所迷惑,该函数内部并不会新创建任何线程,而是会忠实地执行query graph的每一个操作节点。既然SQL语句是过程,则必然会有if-else, for等控制语句, que_run_threads能够执行这些控制语句。因此,整个query graph可以理解为具有若干执行节点的状态机, 而que_run_threads则是这个状态机的执行器。 que_run_threads可以根据query graph中操作节点的关系编排,在各个节点之间跳转,最终完成整个SQL的执行。而row_ins_step,row_upd_step则是query graph中原子操作节点(比如insert node, update node)的原子执行函数,负责执行一个具体的insert/update/delete操作。

所以,代码结合历史我们可以看到,InnoDB作为有独立RDBMS情怀的存储引擎,其作者在最初设计时,是内置了一套词法语法解析+执行计划和执行器的。 但是,现实是InnoDB只是作为MySQL的主要存储引擎,那么问题就来了:InnoDB如何从自身的设计出发,去和MySQL SQL做融合呢?
方法就是写一个融合模块:row0mysql, 将SQL层的write_row/delete_row/rnd_next等api调用,转换成row_ins_step/row_upd_step等调用。 而只要调用了_step函数,则意味着调用方需要生成一个query graph。 这个事情是row0mysql模块来做的。 某个意义上, row0mysql模块可看做是为了调用InnoDB原生的原子操作函数,而编排了一个query graph(当然也做了前置和后置处理操作,以及SQL层格式转换为InnoDB层格式的操作)。

que模块:在上面介绍row模块的内容中,已经展开了对que模块的介绍。总结来说就是:que是innodb作为一个独立rdbms对业务提供的sql执行引擎。在后面对dict模块的介绍中,我们还将进一步讨论que模块。

btr0*: 在InnoDB中,库表和索引结构都是以B树来组织的。 因此row0*层的各种DML操作和Select操作,最终都将转换为对B树的操作。btr0*共五个模块,封装了对B树的操作,是InnoDB中最核心的模块组之一。
要理解btr0*的5个模块作用,还是要回到数据的写入和读取这个根本问题上来。 InnoDB落到磁盘的数据,只有page和redolog两种形态,而一个B树节点其实就是一个page,通过page之间相互串联,最终构成一颗完整的B树。因此,btr0*的作用,就是一个封装层,把众多page捏合成一颗B树,对外提供对B树的CRUD服务。
btr0*有5个模块,可以从insert一条记录这个流程,来理解这5个模块的分工。row_ins_step最终调用了row_ins_clust_index_entry_low,在该函数中调用了3个关键的B树函数,来实现将一条记录插入到B树,分别是:btr_pcur_open,btr_cur_optimistic_insert,btr_cur_pessimistic_insert。
btr_pcur_open的作用是根据要插入的记录,生成一个B树持久化游标,该游标指向了该记录要插入到的page的位置,或者说,游标是某一条记录的的地址,通过这条记录可以确定要插入的记录的位置。一个B树游标在B树模块里,有两种游标,持久化游标(btr_pcur_t)和普通游标(btr_cur_t)。这里p是persistent的意思。因为B树是会分裂和变动的,当B树分裂或合并时,普通游标的位置也会变化。而通过一层对普通游标的封装,将变化对外部屏蔽(当B树没有变化时通过持久化游标的old_rec生成新的地址),既持久化游标。
通过btr_pcur_open获得持久化游标,再获得持久化游标的普通游标,随后就可以调用btr_cur_optimistic_insert和btr_cur_pessimistic_insert去插入记录了。先调用btr_cur_optimistic_insert,该函数视图直接把记录插入到游标指向的page,如果page空间有限,则报错返回,但不尝试分裂page,重构b树然后再插入; 而btr_cur_pessimistic_insert则在page空间有限时,执行page的分裂和B树重构操作,然后再将记录插入。

page0*模块:page0*负责page内部数据的雕琢。B树由若干个page构成,因此对B树的CRUD操作,最终将转换成对page的CRUD。 而这些操作正是由page0*模块来负责。
page0*分三个子模块,page0cur、page0page和page0zip。page0cur包含对page游标的操作。page游标可以理解为指向page内某条记录的指针,page0cur封装了对这个指针的所有操作,包括“删除游标”指向记录,将记录插入到游标指向的地址等。值得注意的是,并没有“修改游标”指向的记录。 这是因为,InnoDB在实现修改记录时,实际上是先执行删除,再执行插入。page0page模块则负责page内部修改之外的其他操作,比如page的创建、page信息打印等。page0zip模块封装了page压缩的操作。

buf0*模块:buf指的是page buffer,buf0*模块组封装了对page buffer的管理操作,包括buf0buf、buf0rea、buf0lru、buf0flu、buf0dblwr、buf0dump、buf0checksum、buf0buddy几个模块。对于SessionThread而言, 在插入/更新记录时,只会将redo log落盘,不将page落盘,只有读page操作而无写page操作。 因此,SessionThread只涉及三个buf模块:buf0buf、buf0rea和buf0lur。buf0buf模块提供新page分配和读page等操作给btr,调用buf0rea的page读函数,从磁盘中读取一个page;buffer pool中的page管理在LRU链中,因此在新page分配和读取page后,需调用buf0lru模块函数来更新LRU链。其他诸如buf0dblwr、buf0flu、buf0dump等模块,主要负责写page或其他操作,在SesstionThread并未用到。

buf模块收到btr模块的读page请求后,如果该page在缓存中,则直接从缓存中取出并返回;否则,将调用fil0fil模块,去磁盘中读取该page然后返回。值得一提的是,在这个场景下,fil0fil是采用同步而非异步的方式读取page。

fsp0*模块:在记录读写的主路径上, 可以看到明显的分层:row0*、btr0*和page0*这几个模块调用buf模块获取page,通过page模块修改page。 而buf先从buffer pool查找该page,如果没有,则调用fil0fil从磁盘中读取该page。总结一下就是说:row0*、btr0*和page0*是page对象的使用者和加工者,buf模块和fil0fil是page对象读取者。那么问题来了:谁负责分配page对象?
负责分配page对象的模块正是fsp。 fsp,顾名思义是表空间(file space),实际上该模块内部包括两个重要的物理存储结构:表空间和段。
表空间是承接MySQL逻辑存储结构(库、表、索引、记录、字段)和物理存储结构(文件、段、区、page、B树、记录、字段)的枢纽:
1.多个表构成一个库;
2.多个表可存储于一个表空间中(系统表空间或共享表空间),也可以一个表对应一个表空间(独立表空间);
3.表空间由多个物理文件构成,物理文件被格式化成若干个page;
4.一个表或一个索引对应一个段,段由若干个page构成,段需要page时,会找表空间管理器分配;
5.当持续往索引段插入记录时, 需要新的page,而新的page由段管理器来分配。

fsp0*包含两个重要的对象:表空间和段。可以将fsp_ 为前缀的函数集视为表空间管理器(核心数据结构为fsp_header_t等);将fseg_ 为前缀的函数视为段管理器(核心数据结构为fseg_inode_t等)。根据上面1-5点所述,可以容易看出段管理器和表空间管理器的作用:
1.段管理器维护一个段,提供段的创建、释放、扩容、缩容;提供页的分配,回收操作;
2.表空间管理器维护一个表空间。提供表空间的创建、释放、扩容、缩容;为段提供page/区的分配、回收。

fil0fil模块:fil目录下只有一个fil0fil.cc,也就是说只有一个模块fil0fil。fil0fil提供所有对物理文件的操作接口,包括创建文件、删除文件、打开文件、读写文件,以及表空间的创建、删除等操作。其中最重要的函数是fil_io, InnoDB所有读写文件的操作,不管是page的读写(表数据page、ibuf page、undo log page等),还是redo log读写,最终都将调用该函数。

os0file模块:os目录下包含多个模块,SesstionThread中也会调用多个模块的代码。但为了体现数据读写这一主干流程,在SesstionThread诸模块交互示意图上,只列出os0file这一模块。
os0file模块供fil0fil调用,它屏蔽了各种操作系统在文件创建和读写上的差异,对上层提供同一个接口。同时,os0file还提供同步读写和异步读写两种机制,供上层调用。

log0log模块:log目录下包含两个模块,log0log和log0recv。SessionThread只涉及log0log模块中创建redo log和事务提交时redo log刷盘两大逻辑。而log0recv则用于在MySQL实例崩溃恢复时根据redo log来恢复page,属于MySQL主线程main thread的工作范畴。在log0log模块中,还包括由srv_master_thread调用的,定时刷redo log的逻辑,但是该逻辑并不会被SessionThread调用。
每一次page的修改,都将生成redo log,因此redo log是不停向前推进的。在redo log的核心对象log_sys(数据结构为log_t)中,用几个lsn变量,包括lsn、write_lsn、current_flush_lsn、flushed_to_disk_lsn、last_checkpoint_lsn、next_checkpoint_lsn来表示redo log推进到的位置、写入到磁盘的位置、刷入到磁盘的位置和做了checkpoint的位置。整个基于lsn的redo log生成、写盘、刷盘和checkpoint机制非常复杂,而且mysql5.6和mysql5.7的实现还不一样,据说mysql8.0仍然在变化。读者在阅读log0log模块时,建议只了解其大概,不必深入了解其是实现细节,确实非常复杂。

mtr0*模块:mtr目录下面有两个模块:mtr0mtr和mtr0log。前面提到,每一次page的修改,都将生成redo log。修改page地方有很多,从最上层的row模块到最底层的fil0fil模块,无一不需要修改page。为此innodb提供了mtr0mtr和mtr0log两个模块,封装了生成、保存redolog(将redolog保存到redo log buffer)等操作, 供所有需要记录redo log的场合使用。

通过mtr两个模块生成和保存redo log的基本操作是:

//开启mtr事务:
mtr_start(&mtr);
//生成redolog:
mlog_write_ulint(faddr + FIL_ADDR_PAGE, addr.page, MLOG_4BYTES, mtr);
mlog_write_ulint(faddr + FIL_ADDR_BYTE, addr.boffset,
MLOG_2BYTES, mtr);
//提交mtr事务,提交操作将把mtr中的redolog写入到redolog buffer,并按512字节对齐:
mtr_commit(&mtr);

innodb将生成redolog和保存到redolog buffer的过程,视作一个内部事务,称之为mini transaction。 其中,mtr_start函数初始化一个mtr结构,mlog_write_ulint等函数根据page的变更生成redo log,这些位于mtr buffer中的redo log,最终由mtr_commit函数拷贝到log_sys的全局log buffer中,同时将mtr过程中拿住的latch释放。
值得一提的是,不少比较权威的MySQL内核文档,对mtr的一些描述是不够严谨的。比如阿里数据库的 《数据库内核月报-MySQL · 引擎特性 · InnoDB mini transation》中所述:

Alt text

mtr_commit函数并不会将redo log强制刷盘,大部分情况下,只会把mtr的redo log拷贝到全局redo log buffer中然后返回。只有在全局redolog buffer不够的情况下,mtr_commit会在prepare_write中调用log_buffer_extend进行扩容,扩容操作会涉及redo log的刷盘操作,但刷入磁盘的redo log,并不是mtr_commit的这部分redolog,而是有log_sys这个全局对象通过其内部各种lsn来确定。

trx0*模块:trx0*模块,结合read目录下的read0read, 和lock目录下的lock0*模块, 构成了innodb的事务子系统。事务子系统解决的是多SesstionTHread的并发控制问题,这是一个有专业深度的问题(《事务处理-概念与技术》Jim Gray, Andreas Reuter)。 通过梳理5.7 innodb代码,基本上可以说,sesstionthread中事务子系统的调用方,只有row模块和handler模块。先记住这个结论,后面有时间再展开对事务子系统的专门讨论。

dict0*模块:dict目录下的dict0*对应数据字典子系统。 由于MySQL的插拔式引擎结构,SQL层和InnoDB层各有自己的数据字典,且互不共享。SQL层的数据字典信息存放在frm文件中,而InnoDB层的数据字典,则是以表的方式组织,总共有5个字典表:SYS_TABLES, SYS_TABLE_IDS, SYS_COLUMNS, SYS_INDEXES, SYS_FIELDS。在系统表空间第一个物理文件的第8个页中,记录了这5个数据字典表对应的B树的根页,根据这些根页加载这5个字典表的所有数据到内存,进而可以加载所有表的元数据到内存。

几乎所有的模块,都需要用到数据字典信息,从SessionThread涉及的诸代码模块交互示意图中,我们可以看到这一点(dict0*模块被所有其他模块所调用)。InnoDB在处理DDL操作时,将更新数据字典信息。而这个更新操作特别值得一提。
在上面对row模块的分析中,我们提到了que的作用:一个sql执行引擎。在更新数据字典信息的流程中,重度使用了que这个sql执行引擎。以建表流程为例:

Alt text

分析row_create_table_for_mysql这个函数的实现可以看到,该函数实际上做了两个操作:
1.更新数据字典表SYS_TABLES、SYS_COLUMNS等: 通过tab_create_graph_create函数生成一个更新这些表的执行计划,然后调用pars_complete_graph_for_exec生成执行环境(que_fork_t和que_thr_t对象),最后调用que_run_threads来执行该计划,完成对各数据字典表的修改,并生成对应的数据字典对象(如dict_table_t, dict_index_t, dict_column_t等)到内存中。
2.更新其他系统表:除了SYS_TABLES、SYS_COLUMNS这些数据字典表,一个create table操作可能还需要做其他事情。比如在指定每个表都有独立表空间的情况下,create table操作还需要修改SYS_TABLESPACES和SYS_DATAFILES表(这两个表不属于系统默认加载的5个数据字典表,但仍然属于数据字典表的范畴,是在加载出5个数据字典表之后,再从这5个数据字典表中取出相关字段来加载出这两表)。具体来说,create table操作需要往SYS_TABLESPACES和SYS_DATAFILES表插入相关的记录,来记录新建的用户表对应的物理文件信息。此操作可以通过que模块的一个函数搞定:

Alt text

que_eval_sql可以理解为innodb向用户提供的一个sql处理接口。用户按照innodb的sql规范,写好sql并提交到que_eval_sql,由该函数完成后续的sql解析、执行计划生成和执行。

3.2 关键数据结构和主干函数

SesstionThread中涉及的关键数据结构和主干函数众多,基本上涵盖InnoDB所有主要数据结构和关键函数。要搞清楚这些数据结构和关键函数有两种方法,一个是从逻辑流程入手,去梳理每种场景(insert/select/commit等),把这些流程中函数的调用层次,关键的函数、涉及的数据结构一一列出,分析;第二种是提炼关键的几个数据结构,把这几个数据结构说清,说透,之后再看每种场景(insert/select/commit等)的操作流程,就能一目了然,迎刃而解。正如Brooks所言:“让我看你的流程图但不让我看数据结构(表), 我会仍然搞不明白。给我看你的数据结构,一般我就不再需要你的流程图了,数据结构能让人一目了然。”

而要揭示InnoDB中的主要数据结构,则需要跟main Thread结合起来。main Thread是初始化InnoDB的线程,各种主要数据结构均在main thread被初始化。所以,现在请看下一章:后台线程:main thread相关代码

4. 后台线程:main thread剖析

作为mysql的主线程, main thread负责整个mysql实例的初始化操作。本文档作为innobase引擎的分析报告,只关注innodb引擎的初始化过程。

4.1 innobase plugin如何赋值

在ha_innodb.cc中有以下宏定义:

mysql_declare_plugin(innobase)
{
MYSQL_STORAGE_ENGINE_PLUGIN,
&innobase_storage_engine,
innobase_hton_name,
plugin_author,
"Supports transactions, row-level locking, and foreign keys",
PLUGIN_LICENSE_GPL,
innobase_init, /* Plugin Init */
NULL, /* Plugin Deinit */
INNODB_VERSION_SHORT,
innodb_status_variables_export,/* status variables */
innobase_system_variables, /* system variables */
NULL, /* reserved */
0, /* flags */
},
i_s_innodb_trx,
i_s_innodb_locks,
i_s_innodb_lock_waits,
i_s_innodb_cmp,
i_s_innodb_cmp_reset,
i_s_innodb_cmpmem,
i_s_innodb_cmpmem_reset,
i_s_innodb_cmp_per_index,
i_s_innodb_cmp_per_index_reset,
i_s_innodb_buffer_page,
i_s_innodb_buffer_page_lru,
i_s_innodb_buffer_stats,
i_s_innodb_temp_table_info,
i_s_innodb_metrics,
i_s_innodb_ft_default_stopword,
i_s_innodb_ft_deleted,
i_s_innodb_ft_being_deleted,
i_s_innodb_ft_config,
i_s_innodb_ft_index_cache,
i_s_innodb_ft_index_table,
i_s_innodb_sys_tables,
i_s_innodb_sys_tablestats,
i_s_innodb_sys_indexes,
i_s_innodb_sys_columns,
i_s_innodb_sys_fields,
i_s_innodb_sys_foreign,
i_s_innodb_sys_foreign_cols,
i_s_innodb_sys_tablespaces,
i_s_innodb_sys_datafiles,
i_s_innodb_sys_virtual
mysql_declare_plugin_end;

在编译时会利用以下宏进行展开,展开的结果是对定义在sql_builtin.cc中的全局变量builtin_innobase_plugin进行赋值:

#ifndef MYSQL_DYNAMIC_PLUGIN
#define __MYSQL_DECLARE_PLUGIN(NAME, VERSION, PSIZE, DECLS) \
MYSQL_PLUGIN_EXPORT int VERSION= MYSQL_PLUGIN_INTERFACE_VERSION; \
MYSQL_PLUGIN_EXPORT int PSIZE= sizeof(struct st_mysql_plugin); \
MYSQL_PLUGIN_EXPORT struct st_mysql_plugin DECLS[]= {
#else
#define __MYSQL_DECLARE_PLUGIN(NAME, VERSION, PSIZE, DECLS) \
MYSQL_PLUGIN_EXPORT int _mysql_plugin_interface_version_= MYSQL_PLUGIN_INTERFACE_VERSION; \
MYSQL_PLUGIN_EXPORT int _mysql_sizeof_struct_st_plugin_= sizeof(struct st_mysql_plugin); \
MYSQL_PLUGIN_EXPORT struct st_mysql_plugin _mysql_plugin_declarations_[]= {
#endif
#define mysql_declare_plugin(NAME) \
__MYSQL_DECLARE_PLUGIN(NAME, \
builtin_ ## NAME ## _plugin_interface_version, \
builtin_ ## NAME ## _sizeof_struct_st_plugin, \
builtin_ ## NAME ## _plugin)
#define mysql_declare_plugin_end ,{0,0,0,0,0,0,0,0,0,0,0,0,0}}

builtin_innobase_plugin作为mysql_mandatory_plugins二维数组的一个元素被定义:

ifdef _MSC_VER
extern "C"
#else
extern
#endif
builtin_plugin
builtin_myisam_plugin,
#ifndef EMBEDDED_LIBRARY
builtin_perfschema_plugin,
#endif
builtin_innobase_plugin, builtin_heap_plugin, builtin_csv_plugin, builtin_myisammrg_plugin, builtin_archive_plugin, builtin_blackhole_plugin, builtin_partition_plugin, builtin_federated_plugin, builtin_ngram_parser_plugin, builtin_binlog_plugin, builtin_mysql_password_plugin;
struct st_mysql_plugin *mysql_optional_plugins[]=
{
builtin_archive_plugin, builtin_blackhole_plugin, builtin_partition_plugin, builtin_federated_plugin, builtin_ngram_parser_plugin, 0
};
struct st_mysql_plugin *mysql_mandatory_plugins[]=
{
builtin_binlog_plugin, builtin_mysql_password_plugin, builtin_myisam_plugin,
#ifndef EMBEDDED_LIBRARY
builtin_perfschema_plugin,
#endif
builtin_innobase_plugin, builtin_heap_plugin, builtin_csv_plugin, builtin_myisammrg_plugin, 0
};

最终builtin_innobase_plugin对象的值为:

Alt text

4.2 如何利用builtin_innobase_plugin初始化innobase引擎:

builtin_innobase_plugin全局对象,在进入main函数之前已经初始化完毕。在plugin_register_builtin_and_init_core_se会加载mysql_mandatory_plugins二维数组,逐个对plugin进行初始化。而builtin_innobase_plugin作为mysql_mandatory_plugins的第5号元素,将在plugin_initialize(plugin_ptr)调用中,将被用来初始化innobase。具体堆栈如下:

Alt text

4.3 main thread流程和InnoDB核心数据结构

前面看到,innobase_start_or_create_for_mysql是InnoDB的初始化函数。该函数的主干流程和初始化的核心数据结构如下图所示(为了简单期间,只列出create_new_db=false,既非首次启动InnoDB的流程):

Alt text

图中,右边部分展现了初始化函数innobase_start_or_create_for_mysql的主要步骤;左边是innodb中最核心的几个数据结构(如buf_pool_t、log_t、file_system_t、trx_sys_t等)的初始化函数。

将 左边部分 和 3.1节的 SessionThread涉及的诸代码模块交互示意图 两个图一结合,我们会发现它们是能够一一对应的。 从这个角度我们可以发现,InnoDB的代码架构思路其实非常简单:

1.一个大功能对应一个大的数据结构,系统启动后分配一个全局对象。比如全局redolog对象log_t* log_sys,全局buffer pool数组 buf_pool_t* buf_pool_ptr等。有些模块出于功能需要,会分配1个以上的全局对象。 比如,trx模块先有 trx_sys_t* trx_sys 这个全局对象,trx在运行时undolog需要purge,又分配出一个trx_purge_t* purge_sys全局对象;而出于脏页刷盘的需求,在全局buffer pool数组 buf_pool_t* buf_pool_ptr之后,再分配一个page_cleaner_t* page_cleaner对象,用于多线程脏页刷盘。

2.InnoDB在核心数据结构的设计上,其实是粗糙、没有设计的。可以把这些核心的、超大的数据结构,理解成一个个大型垃圾桶,相关功能需要用到的变量,都往这个垃圾桶里面扔。这也是为什么每个核心数据结构,比如log_t等,变量这么多,眼花缭乱的原因。 这种简单的变量堆砌,必然导致在具体到细节分析时,逻辑难以理解,代码之间耦合度非常高。

3.可以将InnoDB的函数代码,都理解为加工这些核心全局对象的业务逻辑。从这个角度看,InnoDB的业务逻辑代码也是没有设计感的,只是简单地围绕核心全局对象做各种加工,策略非常地简单甚至简陋。 但一般来讲,这种简陋朴实的方法,往往又是一个软件能够长期保持生命力的重要原因。当然会有概念抽象和代码组织非常到位的业务逻辑代码,但如果没看到本质想通透,那唯一的办法就是保持业务逻辑代码的直白和粗陋。 这种方法要强过哪些没有考虑清楚,不够简洁,无病呻吟的抽象。

4.除了全局对象外,上图标黄部分,给出了无状态模块经常用到的几种数据结构:

dtuple_t: 一条记录的内存对象。插入时,SQL层传入byte*, 在innodb内部转换成该对象
dfield_t: 一个字段的内存对象。
btr_cur_t: B树游标;
btr_pcur_t: B树持久化游标。

buf_block_t,buf_page_t: buf_block_t 和buf_pool_t结合,用于buffer pool的管理;一个buf_block_t 对象对应buffer pool中的一个页, buf_block_t 主要由两部分构成:
1.buf_page_t对象:buf_page_t记录了页的控制信息,比如页的页的物理地址、页在lru链表中的位置等;
2.frame指针:指向一个页的内存空间。

buf_frame_t: buf_frame_t的定义是:typedef byte buf_frame_t; 和buf_block_t->frame一样, 一个buf_frame_t指针指向一个页的内存空间。

que_t, que_fork_t, que_thr_t: 这三个数据结构跟innodb内置的sql解析和执行引擎有关。先看三个数据结构的定义:

/* Query graph root is a fork node */
typedef struct que_fork_t que_t;
struct que_fork_t{
que_common_t common; /*!< type: QUE_NODE_FORK */
que_t* graph; /*!< query graph of this node */
ulint fork_type; /*!< fork type */
ulint n_active_thrs; /*!< if this is the root of a graph, the
number query threads that have been
started in que_thr_move_to_run_state
but for which que_thr_dec_refer_count
has not yet been called */
trx_t* trx; /*!< transaction: this is set only in
the root node */
ulint state; /*!< state of the fork node */
que_thr_t* caller; /*!< pointer to a possible calling query
thread */
UT_LIST_BASE_NODE_T(que_thr_t)
thrs; /*!< list of query threads */
/*------------------------------*/
/* The fields in this section are defined only in the root node */
sym_tab_t* sym_tab; /*!< symbol table of the query,
generated by the parser, or NULL
if the graph was created 'by hand' */
pars_info_t* info; /*!< info struct, or NULL */
/* The following cur_... fields are relevant only in a select graph */
ulint cur_end; /*!< QUE_CUR_NOT_DEFINED, QUE_CUR_START,
QUE_CUR_END */
ulint cur_pos; /*!< if there are n rows in the result
set, values 0 and n + 1 mean before
first row, or after last row, depending
on cur_end; values 1...n mean a row
index */
ibool cur_on_row; /*!< TRUE if cursor is on a row, i.e.,
it is not before the first row or
after the last row */
sel_node_t* last_sel_node; /*!< last executed select node, or NULL
if none */
UT_LIST_NODE_T(que_fork_t)
graphs; /*!< list of query graphs of a session
or a stored procedure */
/*------------------------------*/
mem_heap_t* heap; /*!< memory heap where the fork was
created */
};
struct que_thr_t{
que_common_t common; /*!< type: QUE_NODE_THR */
ulint magic_n; /*!< magic number to catch memory
corruption */
que_node_t* child; /*!< graph child node */
que_t* graph; /*!< graph where this node belongs */
que_thr_state_t state; /*!< state of the query thread */
ibool is_active; /*!< TRUE if the thread has been set
to the run state in
que_thr_move_to_run_state, but not
deactivated in
que_thr_dec_reference_count */
/*------------------------------*/
/* The following fields are private to the OS thread executing the
query thread, and are not protected by any mutex: */
que_node_t* run_node; /*!< pointer to the node where the
subgraph down from this node is
currently executed */
que_node_t* prev_node; /*!< pointer to the node from which
the control came */
ulint resource; /*!< resource usage of the query thread
thus far */
ulint lock_state; /*!< lock state of thread (table or
row) */
struct srv_slot_t*
slot; /* The thread slot in the wait
array in srv_sys_t */
/*------------------------------*/
/* The following fields are links for the various lists that
this type can be on. */
UT_LIST_NODE_T(que_thr_t)
thrs; /*!< list of thread nodes of the fork
node */
UT_LIST_NODE_T(que_thr_t)
trx_thrs; /*!< lists of threads in wait list of
the trx */
UT_LIST_NODE_T(que_thr_t)
queue; /*!< list of runnable thread nodes in
the server task queue */
ulint fk_cascade_depth; /*!< maximum cascading call depth
supported for foreign key constraint
related delete/updates */
row_prebuilt_t* prebuilt; /*!< prebuilt structure processed by
the query thread */
};

首先我们已经知道,innodb内部包含一个独立的sql解析和执行引擎,其sql语法也是自己定义的(和mysql有很大不同),而que graph则表示sql解析之后,得到的一个执行计划。然后,从上面代码中注意到这几个细节:

1.que_t定义上面的注释:

/* Query graph root is a fork node */
typedef struct que_fork_t que_t;

可以看到,que_t既表示que graph的数据结构。que_fork_t是que graph的一个节点。但由于que graph的root节点,总是一个que_fork_t,所以que_t=que_fork_t。

2.que_fork_t 和que_thr_t的区别: 粗看之下,两个数据结构很多变量都是相同的。但我们注意到que_thr_t有que_node_t* run_node, que_node_t* prev_node 这样的,代表运行时的对象;而que_fork_t更像一个执行计划。因此初步可以推断, que_fork_t是一个执行计划,具体来说是que graph的根节点; 而que_thr_t则是一个计划执行时的执行环境对象。

这个推断, 从 innodb的内部sql执行函数:que_eval_sql 的实现中,进一步被证实:

Alt text

que_eval_sql的输入是一个sql(第二个参数),输出是这个sql的执行结果。通过parse_sql解析该sql生成que graph。 通过que_fork_start_command将执行计划(graph)转换为执行环境(thr),最后调用que_run_threads执行,在执行过程中会遍历graph的每个节点并调用对应的执行函数。

5. 后台线程:srv_master_thread剖析

Alt text

更多资料见:姜承尧《MySQL技术内幕-InnoDB存储引擎》(注:姜书中内容给出了该thread的主要流程,但只是泛泛讲述,并不深入,最好的方式还是需要深入阅读源码每一个函数)

6. 后台线程:page cleaner thread剖析

Alt text

注:page cleaner coordinator thread 也会做实际的刷脏页操作。如果一个mysql实例配置4个buffer pool instance,那么对应会有1个page cleaner coordinator thread + 3个page clean worker thread。

更多资料见:http://mysql.taobao.org/monthly/2018/09/02/ (注:该文章依据5.7.23代码,和我们使用的5.7.24接近,文中内容描述可信赖)

7. 后台线程:Purge Thread剖析

Alt text

注:purge coordinator thread 也会做实际的purge undolog操作。如果一个mysql实例配置4个purge thread,那么对应会有1个purge coordinator thread + 3个worker thread。

更多资料见:http://mysql.taobao.org/monthly/2017/12/01/ - 垃圾回收Purge线程(注:该文章依据5.6代码,和5.7有出入,准确的流程以上图为主)。